Como configurar una aplicación de angular para que use un token JWT obtenido de una API para autenticarse
Código de ejemploComo API para autenticarnos usaremos una fake API web para facilitar el proceso, normalmente esto debería de estar integrado en nuestro backend.
StoreRestAPIInstalamos el paquete jwt-decode:
npm install jwt-decode
Creamos el archibo src/environments/environment.ts donde indicaremos la dirección del servidor:
export const environment = {
apiBaseUrl: 'https://api.storerestapi.com'
};
Generamos el modulo auth (src/app/auth/auth.module.ts):
ng generate module auth --routing
Actualizamos el archivo src/app/app-routing.module.ts con el siguiente contenido
{
path: 'auth',
loadChildren: () => import('./auth/auth.module').then(m => m.AuthModule)
}
Metemos el ReactiveFormsModule en el auth.module.ts, ya que lo usaremos para el login:
Creamos el archivo src/app/auth/auth.service.ts con el siguiente contenido:
import { HttpClient, HttpEvent } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { environment } from 'src/environments/environment';
import { map } from 'rxjs/operators';
import jwtDecode from 'jwt-decode';
@Injectable({
providedIn: 'root'
})
export class AuthService {
ACCESS_TOKEN = 'access_token';
REFRESH_TOKEN = 'refresh_token';
private userDataSubject: BehaviorSubject<any> = new BehaviorSubject(null);
userData$: Observable<any> = this.userDataSubject.asObservable();
constructor(private http: HttpClient) {
if (localStorage.getItem(this.ACCESS_TOKEN) && localStorage.getItem(this.REFRESH_TOKEN)) {
const access_token = (<string>localStorage.getItem(this.ACCESS_TOKEN));
const refresh_token = (<string>localStorage.getItem(this.REFRESH_TOKEN))
this.userDataSubject.next({access_token, refresh_token, userInfo: this.getUserDataFromToken(access_token)})
}
}
get userData(): any {
return this.userDataSubject.value
}
get userName(): string {
return this.userData.userInfo.name
}
get userRole(): string {
return this.userData.userInfo.role
}
login(email: string, password: string): Observable<any> {
return this.http.post(`${environment.apiBaseUrl}/auth/login`, { email, password }).pipe(
map((res: any) => {
const access_token = res.data.access_token;
const refresh_token = res.data.refresh_token;
this.userDataSubject.next({access_token, refresh_token, userInfo: this.getUserDataFromToken(access_token)});
localStorage.setItem(this.ACCESS_TOKEN, access_token)
localStorage.setItem(this.REFRESH_TOKEN, refresh_token)
return res
})
)
}
logout(): void {
localStorage.removeItem(this.ACCESS_TOKEN);
localStorage.removeItem(this.REFRESH_TOKEN);
this.userDataSubject.next(null);
// Call http logout method for block refresh token
}
generateNewTokens(): Observable<HttpEvent<any>> {
const refresh_token = this.userDataSubject.value?.refresh_token;
return this.http.post(`${environment.apiBaseUrl}/auth/refresh`, { refresh_token }).pipe(
map((res: any) => {
const access_token = res.data.access_token;
const refresh_token = res.data.refresh_token;
this.userDataSubject.next({access_token, refresh_token, userData: this.getUserDataFromToken(access_token)});
localStorage.setItem(this.ACCESS_TOKEN, access_token);
localStorage.setItem(this.REFRESH_TOKEN, refresh_token);
return res
})
)
}
get isAuthenticated(): boolean {
const refresh_token = this.userDataSubject.value?.refresh_token;
if (!refresh_token) {
return false
}
return this.isAuthTokenValid(refresh_token)
}
isAuthTokenValid(token: string): boolean {
const decoded: any = jwtDecode(token);
const expMilSecond: number = decoded?.exp * 1000; // milliseconds
const currentTime = Date.now(); // milliseconds
if (expMilSecond < currentTime) {
return false;
}
return true;
}
getUserDataFromToken(token: string): any {
const decoded: any = jwtDecode(token);
return decoded.data
}
}
En la funcion login() tenemos que tener en cuenta que la respuesta del servidor puede variar el objeto que se nos devuelva, en este caso se devuelve access_token dentro de un objeto data así que podemos acceder con res.data.access_token
Creamos el componente para el login:
ng generate component auth/login
Y dentro del routing (src/app/auth/auth-routing.module.ts) indicamos el nuevo componente:
Dentro del archivo src/app/auth/auth.module.ts nos aseguramos de que el componente esté declarado:
Dentro del archivo src/app/auth/login/login.component.ts introducimos el siguiente contenido:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Observable } from 'rxjs';
import { AuthService } from '../auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {
loginForm!: FormGroup;
requestData$!: Observable<any>;
constructor(private fb: FormBuilder, private authService: AuthService) { }
ngOnInit(): void {
this.requestData$ = this.authService.userData$;
this.loginForm = this.fb.group({
email: ['', [Validators.required, Validators.email]],
password: ['', Validators.required]
})
}
onFormSubmit(): void {
const formData: any = this.loginForm.value;
this.authService.login(formData?.email, formData?.password)
.subscribe((res: any) => {
console.log(res)
// handle success message and redirect to next page
}, (err) => {
console.log(err)
// handle invalid user message
});
}
}
Y en el archivo src/app/auth/login/login.component.html introducimos el siguiente contenido:
<h3>Login Form</h3>
<form [formGroup]="loginForm" (ngSubmit)="onFormSubmit()">
<input formControlName="email" type="email" required placeholder="Email"> <br>
<input formControlName="password" type="password" required placeholder="Password"> <br>
<button [disabled]="this.loginForm.invalid" type="submit">Submit</button>
</form>
<div style="margin-top: 20px;">
<strong>Requester Data</strong>: <br> {{ requestData$ | async | json }}
</div>
A continuación generamos los Interceptors:
ng generate interceptor _services/auth --skip-tests
ng generate interceptor _services/error --skip-tests
En el archivo src/app/app.module.ts añadimos los interceptors como providers y el modulo HttpClientModule:
En el archivo src/app/_services/auth.interceptor.ts introducimos el siguiente contenido:
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { switchMap, take } from 'rxjs/operators';
import { AuthService } from 'src/app/auth/auth.service';
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService:AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// stage 1: Check if request for refresh token
if (request.url.indexOf('/auth/refresh') !== -1) {
return next.handle(request);
}
const data = this.authService.userData;
const accessToken = data?.access_token;
// stage 2: Checking access_token exists(mean user logged in) or not
if (accessToken) {
// stage 3: checking access_token validity
if (this.authService.isAuthTokenValid(accessToken)) {
let modifiedReq = request.clone({
headers: request.headers.append('Authorization', `Bearer ${accessToken}`)
});
return next.handle(modifiedReq)
}
// stage 4: Going to generate new tokens
return this.authService.generateNewTokens()
.pipe(
take(1),
switchMap((res: any) => {
let modifiedReq = request.clone({
headers: request.headers.append('Authorization', `Bearer ${res?.data?.access_token}`)
});
return next.handle(modifiedReq)
})
)
}
return next.handle(request);
}
}
En el archivo src/app/_services/error.interceptor.ts introducimos:
import { Injectable } from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AuthService } from 'src/app/auth/auth.service';
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(request).pipe(catchError((res) => this.errorHandler(res)));
}
private errorHandler(response: any): Observable<any> {
// console.error('root error res', response)
const status = response?.status;
if (status === 401 || status === 403) {
this.authService.logout();
}
const error = response.error;
let message = response.message;
if (typeof error === 'object') {
const keys = Object.keys(error);
if (keys.some(item => item === 'message')) {
message = error.message;
}
} else if (typeof error === 'string') {
message = error;
}
return throwError({ message, status });
}
}
Generamos un guard con el siguiente comando:
ng generate guard _guard/roleBase --skip-tests
Cuando se nos pregunte el tipo de guard indicamos CanActivate
En el archivo src/app/_guard/role-base.guard.ts introducimos el siguiente contenido:
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Router, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from '../auth/auth.service';
@Injectable({
providedIn: 'root'
})
export class RoleBaseGuard {
constructor(private authService: AuthService, private router: Router) {}
canActivate(
route: ActivatedRouteSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
// Stage 1: check user authentication
if (!this.authService.isAuthenticated) {
this.router.navigate(['/auth/login']);
this.authService.logout();
return false;
}
const validRoles = route.data['authorities'] || [];
const userData = this.authService.userData;
// Stage 2: Check user role
if (validRoles != userData.userInfo.role) {
this.router.navigate(['/']);
return false;
}
return true;
}
}
NOTA: El código que se muestra arriba realiza la comparación con un único role, si queremos que una ruta sea accesible por varios roles tenemos que cambiar la linea:
if (validRoles != userData.userInfo.role)
por esta:
if (!validRoles.includes(userData.userInfo.role))
Y en el archivo src/app/user/user-routing.module.ts cambiaríamos:
authorities: 'ROLE_CUSTOMER'
por:
authorities: ['ROLE_CUSTOMER', 'ROLE_ADMIN']
Creamos un modulo de usuario para introducir una página con la que probaremos si estamos logeados o no:
ng generate module user --routing
Modificamos el archivo src/app/app-routing.module.ts:
{
path: 'users',
loadChildren: () => import('./user/user.module').then(m => m.UserModule)
}
Generamos un componente userList que usaremos para probar si estamos logueados:
ng generate component user/userList
En el archivo src/app/user/user-list/user-list.component.ts tendremos el siguiente codigo:
import { Component } from '@angular/core';
import { AuthService } from 'src/app/auth/auth.service';
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
styleUrls: ['./user-list.component.css']
})
export class UserListComponent {
constructor(
public authService: AuthService
)
{}
ngOnInit() {
console.log(this.authService.userData.userInfo.role);
}
}
Y en el archivo src/app/user/user-list/user-list.component.html:
<p>Usuario: {{authService.userName}}</p>
<p>Role: {{authService.userRole}}</p>
En el archivo src/app/user/user-routing.module.ts añadimos el guard para indicar que usuario puede acceder a la ruta de users:
{
path: '',
component: UserListComponent,
canActivate: [RoleBaseGuard],
data: {
authorities: 'ROLE_CUSTOMER'
}
}
Al intentar acceder al enlace del listado de usuarios como no estamos logueados se nos llevará al componente de login y una vez ahí al loguearnos veremos el token que se nos devuelve:
Una vez nos logueamos, al acceder al listado de usuario veremos el nombre y el rol del usuario
JWT | Token | Authentication | Bearer